/************************************************************************************
Copyright : Copyright (c) Facebook Technologies, LLC and its affiliates. All rights reserved.

Licensed under the Oculus Master SDK License Version 1.0 (the "License"); you may not use
the Utilities SDK except in compliance with the License, which is provided at the time of installation
or download, or which otherwise accompanies this software in either electronic or hard copy form.

You may obtain a copy of the License at
https://developer.oculus.com/licenses/oculusmastersdk-1.0/

Unless required by applicable law or agreed to in writing, the Utilities SDK distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
ANY KIND, either express or implied. See the License for the specific language governing
permissions and limitations under the License.
************************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;

/// <summary>
/// Extension of GraphicRaycaster to support ray casting with world space rays instead of just screen-space
/// pointer positions
/// </summary>
[RequireComponent(typeof(Canvas))]
public class OVRRaycaster : GraphicRaycaster, IPointerEnterHandler
{
    [Tooltip("A world space pointer for this canvas")]
    public GameObject pointer;

    public int sortOrder = 0;

    protected OVRRaycaster()
    { }

    [NonSerialized]
    private Canvas m_Canvas;

    private Canvas canvas
    {
        get
        {
            if (m_Canvas != null)
                return m_Canvas;

            m_Canvas = GetComponent<Canvas>();
            return m_Canvas;
        }
    }

    public override Camera eventCamera
    {
        get
        {
            return canvas.worldCamera;
        }
    }

    public override int sortOrderPriority
    {
        get
        {
            return sortOrder;
        }
    }

	protected override void Start()
	{
		if(!canvas.worldCamera)
		{
			Debug.Log("Canvas does not have an event camera attached. Attaching OVRCameraRig.centerEyeAnchor as default.");
			OVRCameraRig rig = FindObjectOfType<OVRCameraRig>();
			canvas.worldCamera = rig.centerEyeAnchor.gameObject.GetComponent<Camera>();
		}
	}

	/// <summary>
	/// For the given ray, find graphics on this canvas which it intersects and are not blocked by other
	/// world objects
	/// </summary>
	[NonSerialized]
    private List<RaycastHit> m_RaycastResults = new List<RaycastHit>();
    private void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList, Ray ray, bool checkForBlocking)
    {
        //This function is closely based on
        //void GraphicRaycaster.Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)

        if (canvas == null)
            return;

        float hitDistance = float.MaxValue;

        if (checkForBlocking && blockingObjects != BlockingObjects.None)
        {
            float dist = eventCamera.farClipPlane;

            if (blockingObjects == BlockingObjects.ThreeD || blockingObjects == BlockingObjects.All)
            {
                var hits = Physics.RaycastAll(ray, dist, m_BlockingMask);

                if (hits.Length > 0 && hits[0].distance < hitDistance)
                {
                    hitDistance = hits[0].distance;
                }
            }

            if (blockingObjects == BlockingObjects.TwoD || blockingObjects == BlockingObjects.All)
            {
                var hits = Physics2D.GetRayIntersectionAll(ray, dist, m_BlockingMask);

                if (hits.Length > 0 && hits[0].fraction * dist < hitDistance)
                {
                    hitDistance = hits[0].fraction * dist;
                }
            }
        }

        m_RaycastResults.Clear();

        GraphicRaycast(canvas, ray, m_RaycastResults);

        for (var index = 0; index < m_RaycastResults.Count; index++)
        {
            var go = m_RaycastResults[index].graphic.gameObject;
            bool appendGraphic = true;

            if (ignoreReversedGraphics)
            {
                // If we have a camera compare the direction against the cameras forward.
                var cameraFoward = ray.direction;
                var dir = go.transform.rotation * Vector3.forward;
                appendGraphic = Vector3.Dot(cameraFoward, dir) > 0;
            }

            // Ignore points behind us (can happen with a canvas pointer)
            if (eventCamera.transform.InverseTransformPoint(m_RaycastResults[index].worldPos).z <= 0)
            {
                appendGraphic = false;
            }

            if (appendGraphic)
            {
                float distance = Vector3.Distance(ray.origin, m_RaycastResults[index].worldPos);

                if (distance >= hitDistance)
                {
                    continue;
                }

                var castResult = new RaycastResult
                {
                    gameObject = go,
                    module = this,
                    distance = distance,
                    index = resultAppendList.Count,
                    depth = m_RaycastResults[index].graphic.depth,

                    worldPosition = m_RaycastResults[index].worldPos
                };
                resultAppendList.Add(castResult);
            }
        }
    }

    /// <summary>
    /// Performs a raycast using eventData.worldSpaceRay
    /// </summary>
    /// <param name="eventData"></param>
    /// <param name="resultAppendList"></param>
    public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
    {
        if (eventData.IsVRPointer())
        {
            Raycast(eventData, resultAppendList, eventData.GetRay(), true);
        }
    }
    /// <summary>
    /// Performs a raycast using the pointer object attached to this OVRRaycaster
    /// </summary>
    /// <param name="eventData"></param>
    /// <param name="resultAppendList"></param>
    public void RaycastPointer(PointerEventData eventData, List<RaycastResult> resultAppendList)
    {
        if (pointer != null && pointer.activeInHierarchy)
        {
            Raycast(eventData, resultAppendList, new Ray(eventCamera.transform.position, (pointer.transform.position - eventCamera.transform.position).normalized), false);
        }
    }


    /// <summary>
    /// Perform a raycast into the screen and collect all graphics underneath it.
    /// </summary>
    [NonSerialized]
    static readonly List<RaycastHit> s_SortedGraphics = new List<RaycastHit>();
    private void GraphicRaycast(Canvas canvas, Ray ray, List<RaycastHit> results)
    {
        //This function is based closely on :
        // void GraphicRaycaster.Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, List<Graphic> results)
        // But modified to take a Ray instead of a canvas pointer, and also to explicitly ignore
        // the graphic associated with the pointer

        // Necessary for the event system
        var foundGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
        s_SortedGraphics.Clear();
        for (int i = 0; i < foundGraphics.Count; ++i)
        {
            Graphic graphic = foundGraphics[i];

            // -1 means it hasn't been processed by the canvas, which means it isn't actually drawn
            if (graphic.depth == -1 || (pointer == graphic.gameObject))
                continue;
            Vector3 worldPos;
            if (RayIntersectsRectTransform(graphic.rectTransform, ray, out worldPos))
            {
                //Work out where this is on the screen for compatibility with existing Unity UI code
                Vector2 screenPos = eventCamera.WorldToScreenPoint(worldPos);
                // mask/image intersection - See Unity docs on eventAlphaThreshold for when this does anything
                if (graphic.Raycast(screenPos, eventCamera))
                {
                    RaycastHit hit;
                    hit.graphic = graphic;
                    hit.worldPos = worldPos;
                    hit.fromMouse = false;
                    s_SortedGraphics.Add(hit);
                }
            }
        }

        s_SortedGraphics.Sort((g1, g2) => g2.graphic.depth.CompareTo(g1.graphic.depth));

        for (int i = 0; i < s_SortedGraphics.Count; ++i)
        {
            results.Add(s_SortedGraphics[i]);
        }
    }
    /// <summary>
    /// Get screen position of worldPosition contained in this RaycastResult
    /// </summary>
    /// <param name="worldPosition"></param>
    /// <returns></returns>
    public Vector2 GetScreenPosition(RaycastResult raycastResult)
    {
        // In future versions of Uinty RaycastResult will contain screenPosition so this will not be necessary
        return eventCamera.WorldToScreenPoint(raycastResult.worldPosition);
    }


    /// <summary>
    /// Detects whether a ray intersects a RectTransform and if it does also
    /// returns the world position of the intersection.
    /// </summary>
    /// <param name="rectTransform"></param>
    /// <param name="ray"></param>
    /// <param name="worldPos"></param>
    /// <returns></returns>
    static bool RayIntersectsRectTransform(RectTransform rectTransform, Ray ray, out Vector3 worldPos)
    {
        Vector3[] corners = new Vector3[4];
        rectTransform.GetWorldCorners(corners);
        Plane plane = new Plane(corners[0], corners[1], corners[2]);

        float enter;
        if (!plane.Raycast(ray, out enter))
        {
            worldPos = Vector3.zero;
            return false;
        }

        Vector3 intersection = ray.GetPoint(enter);

        Vector3 BottomEdge = corners[3] - corners[0];
        Vector3 LeftEdge = corners[1] - corners[0];
        float BottomDot = Vector3.Dot(intersection - corners[0], BottomEdge);
        float LeftDot = Vector3.Dot(intersection - corners[0], LeftEdge);
        if (BottomDot < BottomEdge.sqrMagnitude && // Can use sqrMag because BottomEdge is not normalized
            LeftDot < LeftEdge.sqrMagnitude &&
                BottomDot >= 0 &&
                LeftDot >= 0)
        {
            worldPos = corners[0] + LeftDot * LeftEdge / LeftEdge.sqrMagnitude + BottomDot * BottomEdge / BottomEdge.sqrMagnitude;
            return true;
        }
        else
        {
            worldPos = Vector3.zero;
            return false;
        }
    }


    struct RaycastHit
    {
        public Graphic graphic;
        public Vector3 worldPos;
        public bool fromMouse;
    };


    /// <summary>
    /// Is this the currently focussed Raycaster according to the InputModule
    /// </summary>
    /// <returns></returns>
    public bool IsFocussed()
    {
        OVRInputModule inputModule = EventSystem.current.currentInputModule as OVRInputModule;
        return inputModule && inputModule.activeGraphicRaycaster == this;
    }

    public void OnPointerEnter(PointerEventData e)
    {
        if (e.IsVRPointer())
        {
            // Gaze has entered this canvas. We'll make it the active one so that canvas-mouse pointer can be used.
            OVRInputModule inputModule = EventSystem.current.currentInputModule as OVRInputModule;
            if(inputModule != null)
            {
                inputModule.activeGraphicRaycaster = this;
            }

        }
    }
}
